import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;

import java.util.HashMap;
import java.util.Map;

public class Tag
{
    /**
     * List of functions for prefix lexemes.
     */
    public String prefix = "";

    /**
     * List of functions for suffix lexemes.
     */
    public String suffix = "";

    /**
     * Part of speech
     */
    public String base = "";

    /**
     * Set of attributes, depending on POS
     */
    public HashMap<String, String> attributes = new HashMap<String, String>();

    /**
     * Construct from analysis DOM element.
     */
    public static Tag GetFromXmlAnalysis(Element analysisElement)
    {
        Tag tag = new Tag();

        // Base
		NodeList baseNodes = analysisElement.getElementsByTagName("base");
        NodeList prefixNodes = analysisElement.getElementsByTagName("prefix");
        NodeList suffixNodes = analysisElement.getElementsByTagName("suffix");

        if( baseNodes.getLength() != 0 )
        {
            // Suppose we have a single 'base' element
            Element baseElem = (Element)baseNodes.item(0);

            // There must be only a single inner element whose name is the tag
            Element tagElem = (Element)baseElem.getChildNodes().item(0);

            tag.base = tagElem.getTagName();

            // Handle participle differently
            if (tag.base.equals("participle"))
            {
                String participleType = tagElem.getAttribute( "participleType" );
                String type = tagElem.getAttribute( "type" );
                if( !participleType.isEmpty() )
                    tag.base = participleType;
                else if( !type.isEmpty() )
                    tag.base = type;
                else
                    tag.base = "participle-unknown";
            }

            // Extract attributes
            if (tag.base.contains("noun") && !tag.base.contains("pronoun") 
                    || tag.base.contains("adjective"))
            {
                tag.AddAttribute("definiteness", tagElem.getAttribute("definiteness"), "false");
                tag.AddAttribute("status", tagElem.getAttribute("status"), "absolute");
            }
            if (tag.base.contains("properName"))
                tag.AddAttribute("definiteness", tagElem.getAttribute("definiteness"), "false");
            
            // Handle finite/infinitive verbs
            if (tag.base.contains("verb"))
            {
                if (tagElem.getAttribute("tense").equals("infinitive"))
                    tag.AddAttribute("tense", "infinitive", null);
                else
                    tag.AddAttribute("tense", "finite", null);
            }
        }

        // Extract prefix/suffix
        if( prefixNodes.getLength() > 0 )
                for (int i = 0; i < prefixNodes.getLength(); i++)
                {
                    Element prefixElement = (Element) prefixNodes.item(i);
                    String function = prefixElement.getAttribute("function");
                    assert !function.isEmpty();
                    if (!tag.prefix.isEmpty())
                        tag.prefix += "+";
                    tag.prefix += function;
                }
            //tag.prefix = "true";
        if( suffixNodes.getLength() > 0 )
                for (int i = 0; i < suffixNodes.getLength(); i++)
                {
                    Element suffixElement = (Element) suffixNodes.item(i);
                    String function = suffixElement.getAttribute("function");
                    if (function.isEmpty())
                        function = "someSuffix";
                    if (!tag.suffix.isEmpty())
                        tag.suffix += "+";
                    tag.suffix += function;
                }
            //tag.suffix = "true";

        return GetFromHash(tag);
    }

    /**
     * Construct from tag DOM element (in rule XML file).
     */
    public static Tag FromXmlTag(Element tagElement)
    {
        Tag tag = new Tag();
        tag.prefix = tagElement.getAttribute("prefix");
        tag.suffix = tagElement.getAttribute("suffix");
        // check if base tag present
        if (tagElement.getChildNodes().getLength() > 0)
        {
            Element baseElement = (Element) tagElement.getChildNodes().item(0);
            tag.base = baseElement.getTagName();
            NamedNodeMap attributeNodes = baseElement.getAttributes();
            for (int i = 0; i < attributeNodes.getLength(); i++)
                tag.AddAttribute(attributeNodes.item(i).getNodeName(), attributeNodes.item(i).getNodeValue(), "");
        }
        return GetFromHash(tag);
    }

     /**
      * Compare to correct tag in reference corpus.
      * @return empty string on tag match, otherwise string describing differences.
      */
    public String CompareToReference(Tag referenceTag)
    {
        StringBuffer buffer = new StringBuffer();

        if(!this.prefix.equals(referenceTag.prefix))
            Utils.append(buffer, "prefix", ",");

        if(!this.suffix.equals(referenceTag.suffix))
            Utils.append(buffer, "suffix", ",");

        // compare bases
        // we need 'contains' for handling cases like 'noun or adjective' in participle
        if(referenceTag.base.contains(this.base)
                && !(referenceTag.base.equals("pronoun") && this.base.equals("noun"))
                && !(referenceTag.base.equals("adverb") && this.base.equals("verb"))
                )
        {
            // compare attributes
            boolean different = false;
            for(String attributeName : this.attributes.keySet())
            {
                String attribute = this.attributes.get(attributeName);
                String referenceAttribute = referenceTag.attributes.get(attributeName);
                assert referenceAttribute != null;
                if(!attribute.equals(referenceAttribute))
                {
                    if(!different)
                    {
                        buffer.append(base).append(":");
                        different = true;
                    }
                    buffer.append("," + attributeName);
                }
            }
        }
        else
            buffer.append("different-tags:" + this.base + "," + referenceTag.base);

        return buffer.toString();
    }

    /**
     * Build DOM element.
     */
    public Element ToXmlElement(Document document, String name)
    {
        Element tagElement = document.createElement(name);
        if (!prefix.isEmpty())
            tagElement.setAttribute("prefix", prefix);
        if (!suffix.isEmpty())
            tagElement.setAttribute("suffix", suffix);
        // check if base tag present
        if (!base.equals(""))
        {
            Element baseElement = document.createElement(base);
            tagElement.appendChild(baseElement);
            for (Map.Entry<String,String> attribute : attributes.entrySet())
                baseElement.setAttribute(attribute.getKey(), attribute.getValue());
        }
        return tagElement;
    }

    
    @Override
    public String toString()
    {
        StringBuffer buffer = new StringBuffer();
        buffer.append(prefix);
        Utils.append(buffer, base, "+");
        Utils.append(buffer, attributes, "");
        Utils.append(buffer, suffix, "+");
        return buffer.toString();
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Tag tag = (Tag) o;

        if (!prefix.equals(tag.prefix)) return false;
        if (!suffix.equals(tag.suffix)) return false;
        if (!attributes.equals(tag.attributes)) return false;
        if (!base.equals(tag.base)) return false;

        return true;
    }

    @Override
    public int hashCode()
    {
        int result = prefix != null ? prefix.hashCode() : 0;
        result = 31 * result + (suffix != null ? suffix.hashCode() : 0);
        result = 31 * result + (base != null ? base.hashCode() : 0);
        result = 31 * result + (attributes != null ? attributes.hashCode() : 0);
        return result;
    }

    /*
    * Private
    */

    /**
     * Private, used to avoid empty tag construction.
     */
    private Tag()
    { }

    /**
     * Add attribute with 'value' or 'defaultValue' if 'value' is null
     */
    private void AddAttribute(String attribute, String value, String defaultValue)
    {
        if (value != null && !value.isEmpty())
            attributes.put(attribute, value);
        else
            attributes.put(attribute, defaultValue);
    }

    /**
     * Global hash of all tags.
     * Speeds up the entire process because only one instance is created for each
     * different tag.
     */
    private static HashMap<Tag, Tag> allTagsMap = new HashMap<Tag, Tag>();

    /**
     * Use existing copy of tag or add the new one to the hash.
     */
    private static Tag GetFromHash(Tag tag)
    {
        Tag result = allTagsMap.get(tag);
        if (result == null)
        {
            allTagsMap.put(tag, tag);
            result = tag;
        }
        return result;
    }
}
